<?php

class GenericSparql {

  protected $client;
  protected $graph;
  protected $graphClause;
  protected $model;

  public function __construct($endpoint, $graph, $model) {
    $this->client = new EasyRdf_Sparql_Client($endpoint);
    $this->graph = $graph;
    $this->model = $model;

    // set graphClause so that it can be used by all queries
    if ($graph[0] == '?') // default endpoint; query any graph (and catch it in a variable)
      $this->graphClause = "GRAPH $graph";
    elseif ($graph)       // query a specific graph
      $this->graphClause = "GRAPH <$graph>";
    else                  // query the default graph
      $this->graphClause = "";
  }

  private function guessVocabID($uri) {
    try {
      $exvoc = $this->model->guessVocabularyFromURI($uri);
    } catch (Exception $e) {
      trigger_error($e->getMessage(), E_USER_WARNING);
      return "???";
    }
    $exvocab = $exvoc->getId();
    return $exvocab;
  }

  public function getGraph() {
    return $this->graph;
  }

  /**
   * Used for counting number of concepts in a vocabulary.
   * @return int number of concepts in this vocabulary
   */
  public function countConcepts() {
    $gc = $this->graphClause;
    $query = <<<EOQ
      SELECT (COUNT(?conc) as ?c) WHERE {
        $gc {
          { ?conc a skos:Concept }
        }
      }
EOQ;
    $result = $this->client->query($query);
    foreach ($result as $row) {
      return $row->c->getValue();
    }
  }

  /**
   * Counts the number of concepts in a easyRDF graph with a specific language.
   * @param array $langs Languages to query for
   * @return Array containing count of concepts for each language and property.
   */
  public function countLangConcepts($langs) {
    $gc = $this->graphClause;
    $ret = array();

    $TERMPROPS = array('skos:prefLabel', 'skos:altLabel', 'skos:hiddenLabel');
    $values = $this->formatValues('?lang', $langs, 'literal');
    foreach ($TERMPROPS as $prop) {
      $query = <<<EOQ
        SELECT ?lang 
          (COUNT(?label) as ?count)
        WHERE {
          $gc {
            ?conc a skos:Concept .
            ?conc $prop ?label .
            FILTER (langMatches(lang(?label), ?lang))
            $values
          }
        }
        GROUP BY ?lang
EOQ;
      // Count the number of terms in each language
      $result = $this->client->query($query);
      // set default count to zero; overridden below if query found labels
      foreach ($langs as $lang) {
        $ret[$lang][$prop] = 0;
      }
      foreach ($result as $row) {
        if (isset($row->lang))
          $ret[$row->lang->getValue()][$prop] = $row->count->getValue();
      }
    }
    ksort($ret);
    return $ret;
  }

  /**
   * Formats a VALUES clause (SPARQL 1.1) which states that the variable should be bound to one
   * of the constants given.
   * @param string $varname variable name, e.g. "?uri"
   * @param array $values the values
   * @param string $type type of values: "uri", "literal" or null (determines quoting style)
   */
  public function formatValues($varname, $values, $type = null) {
    $constants = array();
    foreach ($values as $val) {
      if ($type == 'uri') $val = "<$val>";
      if ($type == 'literal') $val = "'$val'";
      $constants[] = "($val)";
    }
    $values = implode(" ", $constants);
    return "VALUES ($varname) { $values }";
  }

  /**
   * Returns information (as a graph) for one or more concept URIs
   * @param mixed $uris concept URI (string) or array of URIs
   * @param bool $globalCollections whether to use global skos:Collections (true) or not (false)
   * @return EasyRDF_Graph query result graph
   */
  public function queryConceptInfo($uris, $globalCollections = false, $lang = null, $vocid = null, $rest = false) {
    $gc = $this->graphClause;

    // if just a single URI is given, put it in an array regardless
    if (!is_array($uris))
      $uris = array($uris);

    $values = $this->formatValues('?uri', $uris, 'uri');

    if ($globalCollections) {
      // add information about which Collection this concept belongs to
      $construct = "?uri onki:memberOf ?x . ?x skos:prefLabel ?xl . ";
      $optional  = "OPTIONAL { ?x skos:member ?uri . ?x skos:prefLabel ?xl . }";
    } else {
      // add information that can be used to format narrower concepts by
      // the Collection they belong to ("milk by source animal" use case)
      $construct = "?uri skos:narrower ?x . ?x skos:member ?n . ?x skos:prefLabel ?xl . ";
      $optional  = "OPTIONAL { 
                      ?uri skos:narrower ?n .
                      ?x skos:member ?n .
                      ?x skos:prefLabel ?xl . 
                    }";
    }

    $query = <<<EOQ
      CONSTRUCT {
       ?uri ?p ?o .
       ?o rdf:type ?ot .
       ?type rdfs:label ?typelabel .
       ?o skos:prefLabel ?ol .
       $construct  
      } WHERE {
        $gc { 
          ?uri ?p ?o .
          OPTIONAL { ?uri rdf:type ?type .
                     ?type rdfs:label ?typelabel . }
          OPTIONAL { ?o rdf:type ?ot . }
          OPTIONAL { ?o skos:prefLabel ?ol . }
          $optional
        }
      }
      $values
EOQ;
    $result = $this->client->query($query);
    if ($rest)
      return $result;

    if ($result->isEmpty())
      return;

    $conceptArray = array();
    foreach ($uris as $uri) {
      $conc = $result->resource($uri);
      $vocab = $this->model->guessVocabularyFromUri($uri);
      $conceptArray[$uri] = new Concept($this->model, $vocab, $conc);
    }

    return $conceptArray;
  }

  /**
   * Retrieve information about types from the endpoint
   * @return array Array with URIs (string) as key and array of (label, superclassURI) as value
   */
  
  public function queryTypes($lang) {
    $gc = $this->graphClause;
    $query = <<<EOQ
      SELECT DISTINCT ?type ?label ?superclass
      WHERE {
        $gc {
          {
            { ?type rdfs:subClassOf* skos:Concept . }
            UNION
            { ?type rdfs:subClassOf* skos:Collection . }
          }
          OPTIONAL {
            ?type rdfs:label ?label .
            FILTER(langMatches(lang(?label), '$lang'))
          }
          OPTIONAL {
            ?type rdfs:subClassOf ?superclass .
          }
          FILTER EXISTS {
            ?s a ?type .
            ?s skos:prefLabel ?prefLabel .
          }
        }
      }
EOQ;
    $result = array();
    foreach ($this->client->query($query) as $row) {
      $type = array();
      if (isset($row->label)) $type['label'] = $row->label->getValue();
      if (isset($row->superclass)) $type['superclass'] = $row->superclass->getUri();
      $result[$row->type->getURI()] = $type;
    }
    return $result;
  }

  /**
   * Retrieves conceptScheme information from the endpoint.
   * @param string $cs concept scheme URI
   * @return EasyRDF_Graph query result graph
   */
  public function queryConceptScheme($cs) {
    $gc = $this->graphClause;
    $query = <<<EOQ
      CONSTRUCT {
        <$cs> skos:prefLabel ?prefLabel .
        <$cs> dc:title ?title .
        <$cs> dc:description ?desc .
        <$cs> dc:date ?date .
        <$cs> dc:publisher ?publ .
        <$cs> dc:rights ?rights .
        <$cs> owl:versionInfo ?versionInfo .
      } WHERE {
        $gc {
          { <$cs> skos:prefLabel ?preflabel }
          UNION
          { <$cs> dc:title ?title } UNION { <$cs> dc11:title ?title }
          UNION
          { <$cs> dc:publisher ?publ } 
          UNION
          { <$cs> dc:rights ?rights } 
          UNION
          { <$cs> dc:description ?desc } UNION { <$cs> dc11:description ?desc }
          UNION
          { <$cs> dc:date ?date } UNION { <$cs> dc11:date ?date }
          UNION
          { <$cs> owl:versionInfo ?versionInfo }
        }
      }
EOQ;
    return $this->client->query($query);
  }

  /**
   * return a list of skos:ConceptScheme instances in the given graph
   * @param string $lang language of labels
   * @return array Array with concept scheme URIs (string) as keys and labels (string) as values
   */
  public function queryConceptSchemes($lang) {
    $gc = $this->graphClause;
    $query = <<<EOQ
           SELECT ?cs ?label
           WHERE {
             $gc {
               ?cs a skos:ConceptScheme .
               OPTIONAL {
                 ?cs rdfs:label ?label .
                 FILTER(langMatches(lang(?label), '$lang'))
               }
               OPTIONAL {
                 ?cs skos:prefLabel ?preflabel .
                 FILTER(langMatches(lang(?prefLabel), '$lang'))
               }
               OPTIONAL {
                 { ?cs dc11:title ?title }
                 UNION
                 { ?cs dc:title ?title }
                 FILTER(langMatches(lang(?title), '$lang'))
               }
             }
           } ORDER BY ?cs
EOQ;
    $ret = array();
    foreach ($this->client->query($query) as $row) {
      $cs = array();
      if (isset($row->label))
        $cs['label'] = $row->label->getValue();
      if (isset($row->prefLabel))
        $cs['prefLabel'] = $row->prefLabel->getValue();
      if (isset($row->title))
        $cs['title'] = $row->title->getValue();
      $ret[$row->cs->getURI()] = $cs;
    }
    return $ret;
  }

  /**
   * a query for retrieving external labels for exactMatch and such.
   * @param string $exuri concept URI for which to query external labels
   * @return EasyRDF_Sparql_Result query result object
   */
  public function queryExternalLabel($exuri) {
    $gc = $this->graphClause;
    $query = <<<EOQ
          SELECT ?exlabel WHERE {
            $gc {
            <$exuri> skos:prefLabel ?exlabel .}}
EOQ;
    return $this->client->query($query);
  }

  /**
   * Query for concepts using a search term.
   * @param string $term search term
   * @param string $lang language code
   * @param int $limit maximum number of hits to retrieve; 0 for unlimited
   * @param int $offset offset of results to retrieve; 0 for beginning of list
   * @param string $type limit search to concepts of the given type
   * @param string $parent limit search to concepts which have the given concept as parent in the transitive broader hierarchy
   * @param string $group limit search to concepts which are in the given group
   * @return EasyRDF_Sparql_Result query result object
   */
  public function queryConcepts($term, $lang, $limit, $offset, $type, $parent=null, $group=null) {
    $gc = $this->graphClause;
    $gr = $this->graph[0] == '?' ? $this->graph : '';
    $limit = ($limit) ? 'LIMIT ' . $limit : '';
    $offset = ($offset) ? 'OFFSET ' . $offset : '';
    $type = EasyRdf_Namespace::expand($type);

    // extra conditions for label language, if specified
    $labelcond = ($lang) ? "&& langMatches(lang(?label), '$lang')" : "";
    $labelcond2 = ($lang) ? "&& langMatches(lang(?alabel), '$lang')" : "";
    
    // extra conditions for parent and group, if specified
    $parentcond = ($parent) ? "?s skos:broader+ <$parent> ." : "";
    $groupcond = ($group) ? "<$group> skos:member ?s ." : "";

    $orderextra = ($this->graph[0] == '?') ? $this->graph : '';

    if ($term == '0-9') {
      $term = '[0-9]';
    } elseif ($term == '!*') {
      $term = '[^A-Za-zåÅöÖäÄ0-9]';
    }

    while (strpos($term, '**') !== false)
      $term = str_replace('**', '*', $term); // removes futile asterisks

    # make sure regex metacharacters are not passed through
    $term = str_replace('\\', '\\\\', preg_quote($term));
    $term = str_replace('\\\\*', '.*', $term); // just to fix regex-syntax to work as expected

    $query = <<<EOQ
      SELECT DISTINCT ?s ?label ?alabel $gr WHERE {
       $gc {
        {
         {
          ?s skos:prefLabel ?label .
          BIND (?label as ?match)
          ?s rdf:type <$type> .
          $parentcond
          $groupcond
          FILTER (regex(str(?label), '^$term$', "i")
                  $labelcond)
         }
         UNION
         {
          ?s skos:altLabel ?alabel .
          BIND (?alabel as ?match)
          ?s rdf:type <$type> .
          ?s skos:prefLabel ?label .
          $parentcond
          $groupcond
          FILTER (regex(str(?alabel), '^$term$', "i")
                  && !regex(str(?label), '^$term$', "i")
                  && langMatches(lang(?alabel), lang(?label))
                  $labelcond2
                 )
         }
         FILTER NOT EXISTS { ?s owl:deprecated true }
        }
       }

      } ORDER BY lcase(?match) $orderextra
      $limit
      $offset
EOQ;
    return $this->client->query($query);
  }

  /**
   * Query a single property of a concept.
   *
   */
  public function queryProperty($uri, $prop, $lang) {
    $uri = is_array($uri) ? $uri[0] : $uri;
    $gc = $this->graphClause;
    $query = <<<EOQ
      SELECT *
      WHERE {
      	$gc {
          <$uri> $prop ?object .
          ?object skos:prefLabel ?label .
          FILTER (langMatches(lang(?label), "$lang"))
	}
      }
EOQ;
    $result = $this->client->query($query);
    $ret = array();
    foreach ($result as $row) {
      $ret[$row->object->getUri()]['label'] = $row->label->getValue();
    }
    return $ret;
  }
  
  public function queryTransitiveProperty($uri, $prop, $lang, $limit) {
    $uri = is_array($uri) ? $uri[0] : $uri;
    $gc = $this->graphClause;
    $query = <<<EOQ
      SELECT *
      WHERE {
      	$gc {
          <$uri> $prop* ?object .
          ?object skos:prefLabel ?label .
          FILTER (langMatches(lang(?label), "$lang"))
        }
      }
      LIMIT $limit
EOQ;
    $result = $this->client->query($query);
    $ret = array();
    foreach ($result as $row) {
      $val = array('label'=>$row->label->getValue());
      $direct = $this->queryProperty($row->object->getUri(), $prop, $lang);
      if (sizeof($direct) > 0) $val['direct'] = $direct;
      $ret[$row->object->getUri()] = $val;
    }
    return $ret;
  }

  public function queryChildren($uri, $lang) {
    $uri = is_array($uri) ? $uri[0] : $uri;
    $gc = $this->graphClause;
    $query = <<<EOQ
      SELECT ?child ?label ?child ?grandchildren WHERE {
      	$gc {
          <$uri> skos:narrower ?child .
          ?child skos:prefLabel ?label .
          FILTER (langMatches(lang(?label), "$lang"))
          BIND ( EXISTS { ?child skos:narrower ?a . } AS ?grandchildren )
	}
      }
EOQ;
    $ret = array();
    $result = $this->client->query($query);
    foreach ($result as $row) {
      $ret[] = array(
        'uri' => $row->child->getUri(),
        'prefLabel' => $row->label->getValue(),
        'hasChildren' => filter_var($row->grandchildren->getValue(), FILTER_VALIDATE_BOOLEAN),
      );
    }
    return $ret;
  }

  public function queryTopConcepts($conceptScheme='?concept', $lang) {
	 	$gc = $this->graphClause;
    $query = <<<EOQ
			SELECT ?top ?label WHERE {
      	$gc {
          ?top skos:topConceptOf <$conceptScheme> .
          ?top skos:prefLabel ?label .
          FILTER (langMatches(lang(?label), "$lang"))
				}
			}
EOQ;
    $result = $this->client->query($query);
    $ret = array();
    foreach ($result as $row) {
      if(isset($row->top) && isset($row->label)) {
        $ret[$row->top->getUri()] = $row->label->getValue();
      }
    }
    return $ret;
  }

  /**
	 * Query for finding the hierarchy for a concept.
	 * @param string $uri concept uri.
	 * @return an array for the REST controller to encode.
	 */ 
  public function queryParentList($uri, $lang) {
    $orig_uri = $uri;
	 	$gc = $this->graphClause;
    $query = <<<EOQ
      SELECT ?broad ?label ?parent ?member ?exact ?top ?children ?childlabel ?grandchildren WHERE {
      	$gc {
          <$uri> skos:broader*|skos:exactMatch / skos:broader* ?broad .
          ?broad skos:prefLabel ?label .
          FILTER (langMatches(lang(?label), "$lang"))
          OPTIONAL { ?broad skos:broader ?parent . }
          OPTIONAL { ?broad skos:narrower ?children . 
            OPTIONAL { ?children skos:prefLabel ?childlabel . 
              FILTER (langMatches(lang(?childlabel), "$lang"))
            }
          }
          BIND ( EXISTS { ?children skos:narrower ?a . } AS ?grandchildren )
          OPTIONAL { ?broad skos:exactMatch ?exact . }
          OPTIONAL { ?broad skos:topConceptOf ?top . }
	}
      }
EOQ;
    $result = $this->client->query($query);
    $ret = array();
    foreach ($result as $row) {
      $uri = $row->broad->getUri();
      if (!isset($ret[$uri])) {
	$ret[$uri] = array('uri'=>$uri);
      }
      if(isset($row->exact)) {
        $ret[$uri]['exact'] = $row->exact->getUri();
      }
      if(isset($row->top)) {
        $ret[$uri]['top'] = $row->top->getUri();
      }
      if(isset($row->children) && isset($row->childlabel)) {
        if(!isset($ret[$uri]['narrower'])) 
          $ret[$uri]['narrower'] = array();
        $child_arr = array(
          'uri' => $row->children->getUri(),
          'label' => $row->childlabel->getValue(),
          'hasChildren' => filter_var($row->grandchildren->getValue(), FILTER_VALIDATE_BOOLEAN),
        );
        if(!in_array($child_arr, $ret[$uri]['narrower'])) 
          $ret[$uri]['narrower'][] = $child_arr; 
      }
      $ret[$uri]['prefLabel'] = $row->label->getValue();
      if (isset($row->parent) && (isset($ret[$uri]['broader']) && !in_array($row->parent->getUri(), $ret[$uri]['broader']))) {
        $ret[$uri]['broader'][] = $row->parent->getUri();
      } elseif(isset($row->parent) && !isset($ret[$uri]['broader'])) {
        $ret[$uri]['broader'][] = $row->parent->getUri();
      }
    }
    // querying the 'leaf' concepts information too.
    $result = $this->queryConceptInfo($orig_uri, false, $lang); //conceptDAO
    $result = $result[$orig_uri];
    $ret[$result->getUri()]['prefLabel'] = $result->getLabel();
    return $ret;	
  }

  /**
   * return a list of skos:Collection instances, sorted by label
   * @param string $lang language of labels to return
   * @return array Result array with collection URI as key and collection label as value
   */
  public function listCollections($lang) {
    $gc = $this->graphClause;
    $query = <<<EOQ
     SELECT ?coll ?label 
     WHERE { 
       $gc { 
         ?coll a skos:Collection .
         ?coll skos:prefLabel ?label .
         FILTER (langMatches(lang(?label), '$lang'))
       }
     } ORDER BY lcase(?label)
EOQ;
    $ret = array();
    $result = $this->client->query($query);
    foreach ($result as $row) {
      $ret[$row->coll->getURI()] = $row->label->getValue();
    }
    return $ret;
  }

  /**
   * return a list of concepts in a skos:Collection
   * @param string $collection URI of the collection instance
   * @param string $lang language of labels to return
   * @return array Result array with concept URI as key and concept label as value
   */
  public function listCollectionContents($collection, $lang) {
    $gc = $this->graphClause;
    $query = <<<EOQ
     SELECT ?conc ?label 
     WHERE { 
       $gc { 
         <$collection> skos:member ?conc .
         ?conc skos:prefLabel ?label .
         FILTER (langMatches(lang(?label), '$lang'))
       }
     } ORDER BY lcase(?label)
EOQ;
    $ret = array();
    $result = $this->client->query($query);
    foreach ($result as $row) {
      $ret[$row->conc->getURI()] = $row->label->getValue();
    }
    return $ret;
  }
}

?>
